#!/usr/bin/env ruby

#require File.join(File.dirname(__FILE__), '..', 'monkey', 'object')

########################################

if RUBY_VERSION < '1.8'
  # Weird hack 'cause I can't figure out how to alias File.stat class method
  # How do I turn off warnings?
  OLD_FILE = File.clone
  class File < OLD_FILE
    def self.stat(fileName)
      fileName = '.' if fileName.empty?
      OLD_FILE.stat(fileName)
    end
  end
end

########################################

class StorageSize
  include Comparable
  
  # Multiplication factors
  
  POWERS = [
    ['k', 1],  # kilobyte
    ['m', 2],  # megabyte
    ['g', 3],  # gigabyte
    ['t', 4],  # terabyte
    ['p', 5],  # petabyte
    ['e', 6],  # exabyte
    ['z', 7],  # zettabyte
    ['y', 8],  # yottabyte
  ]
  
  def self.multiplier
    @@multiplier
  end
  
  def self.multiplier=(integer)
    unless integer.is_a?(Integer)
      raise TypeError, 
        "#{name}.multiplier must be Integer (#{integer.class})", caller
    end
    unless integer > 0
      raise RangeError, 
        "#{name}.multiplier must be > 0 (#{integer})", caller
    end
    @@multiplier = integer
    POWERS.each do |key, val|
      class_eval("@@#{key}mult = integer**#{val}")
    end
  end
  
  # Initializer / constructor
  
  def initialize(integer)
    unless integer.is_a?(Integer)
      raise TypeError, 
        "#{self.class}.new must be Integer (#{integer.class})", caller
    end
    @bytes = integer
  end
  
  # Factories with conversion: from_k, from_m, from_g, etc.
  
  POWERS.each do |key, val|
    meth_def = <<-FROM_X
      def self.from_#{key}(numeric)
        unless numeric.is_a?(Numeric)
          raise TypeError, 
            "#{name}.from_#{key} must be Numeric (\#{numeric.class})", caller
        end
        if numeric < 0
          raise RangeError, 
            "#{name}.from_#{key} must be >= 0 (\#{numeric})", caller
        end
        return new((numeric * @@#{key}mult).to_i)
      end
    FROM_X
    class_eval(meth_def)
  end
  
  # Factory takes a String, File::Stat, or Pathname
  
  def self.[](pathname_or_filestat)
    x = pathname_or_filestat
    if x.is_a?(String)
      x = '.' if x.empty?
      return new(FileTest.size(x))
    end
    return new(x.size) if x.respond_to?(:file?)
    raise TypeError, 
      "#{name}[] must be Pathname, File::Stat, or String (#{x.class})", caller
  end
  
  # Number outputs
  
  def to_f
    @bytes.to_f
  end
  
  def to_i
    @bytes
  end
  alias :to_b :to_i
  
  # Number outputs with conversion: to_k, to_m, to_g, etc.
  
  POWERS.each do |key, val|
    meth_def = <<-TO_X
      def to_#{key}
        to_f / @@#{key}mult
      end
    TO_X
    class_eval(meth_def)
  end
  
  # String outputs
  
  def to_s
    s = format("%d bytes", @bytes)
    return @bytes.abs == 1 ? s.chop : s
  end
  
  def to_best
    POWERS.reverse.each do |key, val|
      fmt_condition = <<-FORMAT
        if @bytes.abs / @@#{key}mult > 0
          return format("%3.1f #{key.upcase}B", to_#{key})
        end
      FORMAT
      eval(fmt_condition)
    end
    to_s
  end
  
  # Comparison
  
  def <=>(other)
    return @bytes <=> other.to_b if other.is_a?(StorageSize)
    return @bytes <=> other if other.is_a?(Numeric)
    raise TypeError, 
      "can't compare #{self.class} with #{other.class}", caller
  end
  
  # Math
  
  def +(storage_size)
    unless storage_size.is_a?(StorageSize)
      raise TypeError, 
        "can't add #{storage_size.class} to #{self.class}", caller
    end
    self.class.new(@bytes + storage_size.to_b)
  end
  
  def -(storage_size)
    unless storage_size.is_a?(StorageSize)
      raise TypeError, 
        "can't subtract #{storage_size.class} from #{self.class}", caller
    end
    self.class.new(@bytes - storage_size.to_b)
  end
  
  def *(numeric)
    return self.class.new((@bytes * numeric).to_i) if numeric.is_a?(Numeric)
    raise TypeError, 
      "can't multiply #{self.class} by #{numeric.class}", caller
  end
  
  def /(storage_size_or_numeric)
    other = storage_size_or_numeric
    return (@bytes / other.to_b) if other.is_a?(StorageSize)
    return self.class.new((@bytes / other).to_i) if other.is_a?(Numeric)
    raise TypeError, 
      "can't divide #{self.class} by #{other.class}", caller
  end
  
  # Coercion needs inner class
  # Explain this at http://c2.com/cgi/wiki?RubyCoerce & RubyGarden Wiki
  
  class SpecialNumber
    def initialize(numeric)
      @val = numeric
    end
    
    include Comparable
    def <=>(ss)
      @val <=> ss.to_b
    end
    
    def *(ss)
      ss.class.new((@val * ss.to_b).to_i)
    end
    
    def method_missing(op, ss, *args)
      raise TypeError, 
        "#{@val.class} #{op} #{ss.class} not allowed", caller
    end
  end
  
  def coerce(other)
    return [SpecialNumber.new(other), self] if other.is_a?(Numeric)
    raise TypeError, 
      "#{self.class} can't be coerced into #{other.class}", caller
  end
end

/[a-z ]/ =~ 'foo'


StorageSize.multiplier = 1024

########################################

if $0 == __FILE__
#   class_meths = StorageSize.methods - Object.methods
#   p class_meths.sort
#   p StorageSize.instance_methods.sort
#   p StorageSize.class_variables.sort
  
  require 'assert'
  
  S0   = StorageSize.new(  0)
  S2   = StorageSize.new(  2)
  S5   = StorageSize.new(  5)
  S123 = StorageSize.new(123)
  
  puts S0 < S2
  puts S5 > S2
  puts S0 < 2
  puts S5 > 2
  puts 0 < S2
  puts 5 > S2
  
  puts S123 + S0
  puts S123 + S5
  #fail "don't coerce for addition" if (5 + S123) == 128
  
  puts S123 - S0
  puts S123 - S5
  #fail "don't coerce for subtraction" if (128 - S123) == 5
  
  puts S123 * 0
  puts S123 * 5
  puts 5 * S123
  
  p S123 / S2
  p S123 / 2
  #fail "don't coerce for /" if (123 / S2).between?(61, 62)
  
  #fail "dont' coerce for other ops" if (2 % S5) == 2
  
  puts 'all asserts pass :-)'
end

########################################

=begin
#doctest Demo StorageSize.<=>
>> StorageSize.new(2) < StorageSize.new(5)
=> true
>> StorageSize.new(2) < 5
=> true
>> 2 < StorageSize.new(5)
=> true
=end

=begin
#doctest Demo StorageSize.+
>> StorageSize.new(2) + StorageSize.new(5)
=> #<StorageSize:0x16de0c @bytes=7>
>> StorageSize.new(2) + 5
TypeError: can't add Fixnum to StorageSize
>> 2 + StorageSize.new(5)
TypeError: Fixnum + StorageSize not allowed
=end

=begin
#doctest Demo StorageSize.-
>> StorageSize.new(2) - StorageSize.new(5)
=> #<StorageSize:0x167200 @bytes=-3>
>> StorageSize.new(2) - 5
TypeError: can't subtract Fixnum from StorageSize
>> 2 - StorageSize.new(5)
TypeError: Fixnum - StorageSize not allowed
=end

=begin
#doctest Demo StorageSize.*
>> StorageSize.new(2) * StorageSize.new(5)
TypeError: can't multiply StorageSize by StorageSize
>> StorageSize.new(2) * 5.5
=> #<StorageSize:0x15aadc @bytes=11>
>> 5.5 * StorageSize.new(2)
=> #<StorageSize:0x156a90 @bytes=11>
=end

=begin
#doctest Demo StorageSize./
>> StorageSize.new(5) / StorageSize.new(2)
=> 2
>> StorageSize.new(5) / 2.0
=> #<StorageSize:0x1340b8 @bytes=2>
>> 5.0 / StorageSize.new(2)
TypeError: Float / StorageSize not allowed
=end
